/**
 * dbms-sjtu
 * RecordPage.java created at Aug 13, 2011
 */
package dbms.record;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


import dbms.Constants;
import dbms.Debug;
import dbms.buffer.Buffer;
import dbms.disk.Page;
import dbms.disk.PageFormatter;
import dbms.server.DbmsServer;

/**
 * @author weapon
 *
 */
public class RecordPage implements PageFormatter{
	public static final int PAGE_HEADER_SIZE = 4 * Constants.INT_SIZE;
	
	public TableInfo table;
	//public Page page;
	public Buffer buffer;
	
	/**
	 * 0 is usual page
	 * 1 is page 1, master page
	 */
	public int pageType;
	
	/**
	 * Page head: record num, record offset, free byte num, first free record offset
	 * 16 bytes
	 */
	public int recordNum;
	public int firstRecordOffset;
	public int freeByteNum;
	public int firstFreeRecOffset;
	
	/**
	 * record offset array
	 * key: rowid, value: offset
	 */
	private Map<Integer, Integer> recordOffsets = new HashMap<Integer, Integer>();
	
	public RecordPage(Buffer buffer, TableInfo table, int pageType){
		this.table = table;
		this.buffer = buffer;
		this.pageType = pageType;
		
		this.recordNum = 0;
		//this.firstRecordOffset = Constants.BLOCKSIZE;
		this.firstRecordOffset = Constants.BLOCKSIZE - 4; //the last four byte stores the next page
		this.freeByteNum = 0;
		this.firstFreeRecOffset = 0;
		/*if(this.buffer.isUsed()){
			this.initRecordOffsetsMap();
		}*/
	}
	
	/**
	 * it is called by the first time of recordpage used
	 */
	public void initBuffer(){
		if(this.buffer == null){
			Buffer buffer = null;
			if((buffer = DbmsServer.bufferManager.findBufferByBlock(this.table.getFirstPage())) == null){
				buffer = DbmsServer.bufferManager.allocBuffer();
				buffer.connectToBlock(this.table.getFirstPage());	
			}
			this.buffer = buffer;
			this.initRecordOffsetsMap();
		}
	}
	
	
	public void initRecordOffsetsMap(){
		this.recordNum = this.buffer.getInt(this.pageType * Constants.FILE_HEADER + 0);
		this.firstRecordOffset = this.buffer.getInt(this.pageType * Constants.FILE_HEADER + 1 * Constants.INT_SIZE);
		this.freeByteNum = this.buffer.getInt(this.pageType * Constants.FILE_HEADER + 2 * Constants.INT_SIZE);
		this.firstFreeRecOffset = this.buffer.getInt(this.pageType * Constants.FILE_HEADER + 3 * Constants.INT_SIZE);
		//page chain 
		int point = 0, offset, rowid, offsetOffset, pageNum;
		while (true) {
			offsetOffset = this.pageType * Constants.FILE_HEADER + 4
					* Constants.INT_SIZE + point * Constants.INT_SIZE;
			offset = this.buffer.getInt(offsetOffset);
			if (offset == 0 || (offset - offsetOffset) < 4) {
				pageNum = this.buffer.getInt(Constants.BLOCKSIZE
						- Constants.INT_SIZE);
				point = 0;
				while (pageNum != 0) {
					Buffer buffer = new Buffer();
					if((buffer = DbmsServer.bufferManager.findBufferByBlock(pageNum)) == null){
						buffer = DbmsServer.bufferManager.allocBuffer();
						buffer.connectToBlock(pageNum);
					}
					
					
					offsetOffset = point * Constants.INT_SIZE;
					offset = buffer.getInt(point * Constants.INT_SIZE);
					if (offset == 0 || (offset - offsetOffset) < 4) {
						pageNum = buffer.getInt(Constants.BLOCKSIZE
								- Constants.INT_SIZE);
						point = 0;
						continue;
					}
					//free region is marked as Integer.MAX_VALUE
					if(offset != Integer.MAX_VALUE){
						rowid = buffer.getInt(offset);
						this.recordOffsets.put(rowid, offset + pageNum * Constants.BLOCKSIZE);
					}
					point++;
				}
				
				if(this.recordNum != this.recordOffsets.size()){
					DbmsServer.shutdown();
					throw new RuntimeException("Database file is broken.");
				}
				return;
			}
			if(offset != Integer.MAX_VALUE){
				rowid = this.buffer.getInt(offset);
				this.recordOffsets.put(rowid, offset);
			}
			point++;
		}
		
		
		

		// Only one page
		/*if(this.recordNum != 0){
			int offset, rowid;
			for(int i = 0; i < this.recordNum; i++){
				offset = this.buffer.getInt(this.pageType * Constants.FILE_HEADER + 4 * Constants.INT_SIZE + i * Constants.INT_SIZE);
				
				
				//if offset is 0, it stands out this is a deleted record
				if(offset != Integer.MAX_VALUE || offset != 0){
					rowid = this.buffer.getInt(offset);
					recordOffsets.put(rowid, offset);
				}
			}
		}*/
	}

	/*
	 * @see dbms.disk.PageFormatter#format(dbms.disk.Page)
	 */
	@Override
	public void format(Page p) {
		p.setInt(0, recordNum);
		p.setInt(1 * Constants.INT_SIZE, firstRecordOffset);
		p.setInt(2 * Constants.INT_SIZE, freeByteNum);
		p.setInt(3 * Constants.INT_SIZE, firstFreeRecOffset);
	}
	
	/**
	 * insert into a record, now support <b style="color:red">page chain</b>
	 */
	public void insert(Record record){
		int pos = 0;
		if(record.rowId == 0){
			record.rowId = this.recordNum + 1;
		}
		this.recordNum += 1;
			
		
		int page = this.firstRecordOffset / Constants.BLOCKSIZE;
		int pageOffset = this.firstRecordOffset % Constants.BLOCKSIZE;
		Buffer pageBuffer = new Buffer();
		int point = 0, offsetOffset, recOffset;
		if(page == 0){
			pageBuffer = this.buffer;
			while(true){
				offsetOffset = point * Constants.INT_SIZE + this.pageType * Constants.FILE_HEADER + 4 * Constants.INT_SIZE;
				recOffset = pageBuffer.getInt(offsetOffset);
				if((recOffset - offsetOffset) < 4 || recOffset == 0){
					break;
				}
				point++;
			}
		} else {
			if((pageBuffer = DbmsServer.bufferManager.findBufferByBlock(page)) == null){
				pageBuffer = DbmsServer.bufferManager.allocBuffer();
				pageBuffer.connectToBlock(page);
			}
			while(true){
				offsetOffset = point * Constants.INT_SIZE;
				recOffset = pageBuffer.getInt(offsetOffset);
				if((recOffset - offsetOffset) < 4 || recOffset == 0){
					break;
				}
				point++;
			}
		}
		
		/*int point = 0, offsetOffset, recOffset;
		while(true){
			offsetOffset = point * Constants.INT_SIZE + this.pageType * Constants.FILE_HEADER + 4 * Constants.INT_SIZE;
			recOffset = pageBuffer.getInt(offsetOffset);
			if((recOffset - offsetOffset) < 4 || recOffset == 0){
				break;
			}
			point++;
		}*/
		
		/**
		 * data overflow, so we should alloc a new page for this table's data
		 */
		if((pageOffset - offsetOffset) < record.recordLength){
			Debug.Error("fuck you, GFW");
			int pageNum = 0;
			if(DbmsServer.freePageList.isEmpty()){
				pageNum = DbmsServer.dataBaseFile.size();
			} else {
				pageNum = DbmsServer.freePageList.remove(0);
			}
			//store the next page on the last four bytes
			pageBuffer.setInt(Constants.BLOCKSIZE - Constants.INT_SIZE, pageNum);
			Buffer buffer = DbmsServer.bufferManager.allocBuffer();
			buffer.connectToBlock(pageNum);
			buffer.page.write(pageNum);
			//step 2: write record data to this page
			int offset = Constants.BLOCKSIZE - record.recordLength - Constants.INT_SIZE;
			buffer.setInt(0, offset);
			
			
			buffer.setInt(offset, record.rowId);
			buffer.setInt(offset + 1 * Constants.INT_SIZE, record.dataByteNum);
			buffer.setInt(offset + 2 * Constants.INT_SIZE, record.recHeadSize);
			pos += 3 * Constants.INT_SIZE;
			for(Integer integer : record.types){
				buffer.setInt(offset + pos, integer);
				pos += Constants.INT_SIZE;
			}
			for(Object object : record.data){
				if(object instanceof Integer){
					buffer.setInt(offset + pos, (Integer)object);
					pos += Constants.INT_SIZE;
				} else if(object instanceof Float){
					buffer.setFloat(offset + pos, (Float)object);
					pos += Constants.INT_SIZE;
				} else {
					buffer.setString(offset + pos, (String)object);
					pos += ((String)object).length();
				}
			}
			buffer.setInt(Constants.BLOCKSIZE - Constants.INT_SIZE, 0);
			
			//this.firstRecordOffset = pageNum * Constants.INT_SIZE + offset;
			this.firstRecordOffset = pageNum * Constants.BLOCKSIZE + offset;
			this.recordOffsets.put(record.rowId, this.firstRecordOffset);
			// set page head
			this.buffer.setInt(this.pageType * Constants.FILE_HEADER + 0, this.recordNum);
			this.buffer.setInt(this.pageType * Constants.FILE_HEADER + 1 * Constants.INT_SIZE, this.firstRecordOffset);

		} else {
			/*if(this.freeByteNum >= record.recordLength){
				
			}*/
			pageOffset -= record.recordLength;
			this.firstRecordOffset = page * Constants.BLOCKSIZE + pageOffset;
			recordOffsets.put(record.rowId, this.firstRecordOffset);
			// set page head
			this.buffer.setInt(this.pageType * Constants.FILE_HEADER + 0, this.recordNum);
			this.buffer.setInt(this.pageType * Constants.FILE_HEADER + 1 * Constants.INT_SIZE, this.firstRecordOffset);
			
			// set page record data
			
			pageBuffer.setInt(offsetOffset, pageOffset);
			pageBuffer.setInt(pageOffset , record.rowId);
			pageBuffer.setInt(pageOffset + 1 * Constants.INT_SIZE, record.dataByteNum);
			pageBuffer.setInt(pageOffset + 2 * Constants.INT_SIZE, record.recHeadSize);
			pos += 3 * Constants.INT_SIZE;
			for(Integer integer : record.types){
				pageBuffer.setInt(pageOffset + pos, integer);
				pos += Constants.INT_SIZE;
			}
			for(Object object : record.data){
				if(object instanceof Integer){
					pageBuffer.setInt(pageOffset + pos, (Integer)object);
					pos += Constants.INT_SIZE;
				} else if(object instanceof Float){
					pageBuffer.setFloat(pageOffset + pos, (Float)object);
					pos += Constants.INT_SIZE;
				} else {
					pageBuffer.setString(pageOffset + pos, (String)object);
					pos += ((String)object).length();
				}
			}
		}
	}
	
	/**
	 * change the recordNum and firstRecordOffset, 
	 * @param rowid
	 */
	public void delete(int rowid){
		if(!recordOffsets.containsKey(rowid)){
			Debug.Error("This table has not a record's row id is " + rowid);
		} else {
			int offset = recordOffsets.get(rowid);
			int pageNum = offset/Constants.BLOCKSIZE;
			int point = 0;
			Buffer buffer = new Buffer();
			if(pageNum == 0){
				buffer = this.buffer;
				point = 3;
			} else {
				if((buffer = DbmsServer.bufferManager.findBufferByBlock(pageNum)) == null){
					buffer = DbmsServer.bufferManager.allocBuffer();
					buffer.connectToBlock(pageNum);
				}
				offset = offset % Constants.BLOCKSIZE;
			}
			
			
			
			int offsetOffset = buffer.getInt(point * Constants.INT_SIZE);
			while(offsetOffset != offset){
				point++;
				offsetOffset = buffer.getInt(point * Constants.INT_SIZE);
			}
			
			buffer.setInt(point * Constants.INT_SIZE, Integer.MAX_VALUE);
			
			
			
			
			
			this.firstFreeRecOffset = offset;
			this.freeByteNum = buffer.getInt(offset + Constants.INT_SIZE) + 
			buffer.getInt(offset + 2 * Constants.INT_SIZE) + 3 * Constants.INT_SIZE;
			
			/**
			 * If rowid is at internal, we do nothing, 
			 * else we change offset array and firstRecordOffset. <br>
			 * set offset array's offset is 0
			 *//*
			if(rowid == this.recordNum){
				this.buffer.setInt(this.pageType * Constants.FILE_HEADER + (4 + rowid - 1)*Constants.INT_SIZE, 0);
				//FIX at 8.29
				if(this.recordNum <= 1){
					this.firstRecordOffset = Constants.BLOCKSIZE - Constants.INT_SIZE;
				} else {
					this.firstRecordOffset = this.buffer.getInt(this.pageType * Constants.FILE_HEADER + (4 + rowid - 2)*Constants.INT_SIZE);
				}
			} else {
				//set the deleted offset is integer's max value
				this.buffer.setInt(this.pageType * Constants.FILE_HEADER + (4 + rowid - 1)*Constants.INT_SIZE, Integer.MAX_VALUE);
				this.buffer.setInt(offset, 0); // the next free region is null
				//this.buffer.setInt(offset + 4, ); //the free length
			}*/
		
			this.recordNum -= 1;
			this.buffer.setInt(this.pageType * Constants.FILE_HEADER + 0, this.recordNum);
			//this.buffer.setInt(this.pageType * Constants.FILE_HEADER + 1 * Constants.INT_SIZE, this.firstRecordOffset);
			this.buffer.setInt(this.pageType * Constants.FILE_HEADER + 3 * Constants.INT_SIZE, this.firstFreeRecOffset);
			
			//remove the value of this key 
			this.recordOffsets.remove(rowid);
		}
	}
	
	/**
	 * update a record's column value, now support <b style="color:red">page chain</b>
	 */
	public void update(int rowId, Map<String, Object> newData){
		boolean overflow = false;
		
		int offset = this.recordOffsets.get(rowId);
		int pageNum = offset/Constants.BLOCKSIZE;
		
		//get the buffer of this record stores at
		Buffer buffer = new Buffer();
		if(pageNum == 0){
			buffer = this.buffer;
		} else {
			if((buffer = DbmsServer.bufferManager.findBufferByBlock(pageNum)) == null){
				buffer = DbmsServer.bufferManager.allocBuffer();
				buffer.connectToBlock(pageNum);
			}
			offset = offset % Constants.BLOCKSIZE;
		}
		List<Object> data = new ArrayList<Object>();
		
		
		int rank = 0, type, dataOffset;
		int headSize = buffer.getInt(offset + 2 * Constants.INT_SIZE);
		dataOffset = 3 * Constants.INT_SIZE + headSize + offset;
		//get this record's all data, for data overflow
		for(int i = 0; i < headSize / Constants.INT_SIZE; i++){
			type = buffer.getInt((3 + i) * Constants.INT_SIZE + offset);
			if(type == Constants.INTEGER){
				data.add(buffer.getInt(dataOffset));
				dataOffset += Constants.INT_SIZE;
			} /* else if(type == Constants.DECIMAL) {
				data.add.(buffer.getFloat(dataOffset);
				dataOffset += Constants.INT_SIZE;
			} */ else {
				data.add(buffer.getString(dataOffset, (type - 5)/2));
				dataOffset += (type - 5)/2;
			}
		}
		
		for(String field : newData.keySet()){
			rank = this.table.getTableSchema().getFieldRank().get(field);
			int fieldDataOffset = 3 * Constants.INT_SIZE + headSize;
			//get offset by calculate every data length
			for(int r = 0; r < rank; r++){
				int tempType = buffer.getInt((3 + r) * Constants.INT_SIZE + offset);
				if(tempType == Constants.INTEGER){
					fieldDataOffset += Constants.INT_SIZE;
				} else {
					fieldDataOffset += (tempType - 5)/2;
				}
			}
			type = buffer.getInt(offset + (3 + rank) * Constants.INT_SIZE);
			
			if(type == Constants.INTEGER){
				buffer.setInt(offset + fieldDataOffset, (Integer)newData.get(field));
			} else {
				String valueString = (String)newData.get(field);
				
				if(valueString.length() > (type - 5)/2){
					data.set(rank, valueString);
					overflow = true;
					//buffer.setString(fieldDataOffset + offset, valueString.substring(0, (type - 5)/2));
				} else {
					//buffer.setInt((3 + rank) * Constants.INT_SIZE + offset, valueString.length() * 2 + 5);
					buffer.setString(fieldDataOffset + offset, valueString);
				}
			}
		}
		
		
		//put the overflow record's all data to the tail
		if(overflow){
			Record record = new Record(rowId, data);
			delete(rowId);
			insert(record);
		}
	}
	
	/**
	 * select the data about some fields in table content, now support <b style="color:red">page chain</b>
	 * @param fieldlList: all field name which we want
	 * @return a map which key is rowid, value is a record data list
	 */
	public Map<Integer, List<Object>> select(List<String> fieldlList){
		Map<Integer, List<Object>> result = new HashMap<Integer, List<Object>>();
		for(String field : fieldlList){
			if(!this.table.getTableSchema().getInfo().containsKey(field)){
				Debug.Error("The table(" + this.table.getTableName() + ") has no this field : " + field);
				return null;
			}
		}
		
		this.recordNum = this.buffer.getInt(this.pageType * Constants.FILE_HEADER + 0);
		this.firstRecordOffset = this.buffer.getInt(this.pageType * Constants.FILE_HEADER + 1 * Constants.INT_SIZE);
		this.freeByteNum = this.buffer.getInt(this.pageType * Constants.FILE_HEADER + 2 * Constants.INT_SIZE);
		this.firstFreeRecOffset = this.buffer.getInt(this.pageType * Constants.FILE_HEADER + 3 * Constants.INT_SIZE);
		//only one page
		/*if(this.recordNum != 0){
			int offset, rowid, headSize;
			for(int i = 0; i < this.recordNum; i++){
				offset = this.buffer.getInt(this.pageType * Constants.FILE_HEADER + 4 * Constants.INT_SIZE + i * Constants.INT_SIZE);
				rowid = this.buffer.getInt(offset);
				headSize = this.buffer.getInt(offset + 2 * Constants.INT_SIZE);
				List<Object> tempData = new ArrayList<Object>();
				for(String field : fieldlList){
					//get the rank of this field
					int rank = this.table.getTableSchema().getFieldRank().get(field);
					int fieldOffset = 3 * Constants.INT_SIZE + headSize;
					for(int r = 0; r < rank; r++){
						int tempType = this.buffer.getInt((3 + r) * Constants.INT_SIZE + offset);
						if(tempType == Constants.INTEGER){
							fieldOffset += Constants.INT_SIZE;
						} else {
							fieldOffset += (tempType - 5)/2;
						}
					}
					int type = this.buffer.getInt((3 + rank) * Constants.INT_SIZE + offset);
					if(type == Constants.INTEGER){
						tempData.add(this.buffer.getInt(fieldOffset + offset)); 
					} else {
						tempData.add(this.buffer.getString(fieldOffset + offset, (type - 5)/2));
					}
				}
				result.put(rowid, tempData);
			}
		}*/
		
		//page chain 
		for(Integer integer : this.recordOffsets.keySet()){
			int offset = this.recordOffsets.get(integer);
			int pageNum = offset/Constants.BLOCKSIZE;
			Buffer buffer = new Buffer();
			if(pageNum == 0){
				buffer = this.buffer;
			} else {
				if((buffer = DbmsServer.bufferManager.findBufferByBlock(pageNum)) == null){
					buffer = DbmsServer.bufferManager.allocBuffer();
					buffer.connectToBlock(pageNum);
				}
				offset = offset % Constants.BLOCKSIZE;
			}
			
			int rowid, headSize;
			rowid = buffer.getInt(offset);
			headSize = buffer.getInt(offset + 2 * Constants.INT_SIZE);
			List<Object> tempData = new ArrayList<Object>();
			for(String field : fieldlList){
				//get the rank of this field
				int rank = this.table.getTableSchema().getFieldRank().get(field);
				int fieldOffset = 3 * Constants.INT_SIZE + headSize;
				for(int r = 0; r < rank; r++){
					int tempType = buffer.getInt((3 + r) * Constants.INT_SIZE + offset);
					if(tempType == Constants.INTEGER){
						fieldOffset += Constants.INT_SIZE;
					} else if(tempType == Constants.DECIMAL){
						fieldOffset += Constants.INT_SIZE;
					} else if(tempType == Constants.DOUBLE){
						fieldOffset += Constants.INT_SIZE;
					}else {
						fieldOffset += (tempType - 5)/2;
					}
				}
				int type = buffer.getInt((3 + rank) * Constants.INT_SIZE + offset);
				if(type == Constants.INTEGER){
					tempData.add(buffer.getInt(fieldOffset + offset)); 
				} else if(type == Constants.DECIMAL || type == Constants.DOUBLE){
					tempData.add(buffer.getFloat(fieldOffset + offset));
				} else {
					tempData.add(buffer.getString(fieldOffset + offset, (type - 5)/2));
				}
			}
			result.put(rowid, tempData);
		}
		return result;
	}
	
}
